Added liquid templating and migrated first agents

Dominik Sander 11 年 前
コミット
d9bd6a991b

+ 1 - 0
Gemfile

@@ -22,6 +22,7 @@ gem 'json', '~> 1.8.1'
22 22
 gem 'jsonpath', '~> 0.5.3'
23 23
 gem 'twilio-ruby', '~> 3.11.5'
24 24
 gem 'ruby-growl', '~> 4.1.0'
25
+gem 'liquid', '~> 2.6.1'
25 26
 
26 27
 gem 'delayed_job', '~> 4.0.0'
27 28
 gem 'delayed_job_active_record', '~> 4.0.0'

+ 2 - 0
Gemfile.lock

@@ -148,6 +148,7 @@ GEM
148 148
       activesupport (>= 3.0.0)
149 149
     kramdown (1.3.3)
150 150
     libv8 (3.16.14.3)
151
+    liquid (2.6.1)
151 152
     macaddr (1.7.1)
152 153
       systemu (~> 2.6.2)
153 154
     mail (2.5.4)
@@ -337,6 +338,7 @@ DEPENDENCIES
337 338
   jsonpath (~> 0.5.3)
338 339
   kaminari (~> 0.15.1)
339 340
   kramdown (~> 1.3.3)
341
+  liquid (~> 2.6.1)
340 342
   mysql2 (~> 0.3.15)
341 343
   nokogiri (~> 1.6.1)
342 344
   protected_attributes (~> 1.0.7)

+ 27 - 0
app/concerns/liquid_interpolatable.rb

@@ -0,0 +1,27 @@
1
+module LiquidInterpolatable
2
+  extend ActiveSupport::Concern
3
+
4
+  def interpolate_options options, payload
5
+    duped_options = options.dup.tap do |duped_options|
6
+      duped_options.each_pair do |key, value|
7
+        if value.class == String
8
+          duped_options[key] = Liquid::Template.parse(value).render(payload)
9
+        else
10
+          duped_options[key] = value
11
+        end
12
+      end
13
+    end
14
+    duped_options
15
+  end
16
+
17
+  require 'uri'
18
+  # Percent encoding for URI conforming to RFC 3986.
19
+  # Ref: http://tools.ietf.org/html/rfc3986#page-12
20
+  module Huginn
21
+    def uri_escape(string)
22
+      CGI::escape string
23
+    end
24
+  end
25
+
26
+  Liquid::Template.register_filter(LiquidInterpolatable::Huginn)
27
+end

+ 11 - 10
app/models/agents/event_formatting_agent.rb

@@ -1,5 +1,6 @@
1 1
 module Agents
2 2
   class EventFormattingAgent < Agent
3
+    include LiquidInterpolatable
3 4
     cannot_be_scheduled!
4 5
 
5 6
     description <<-MD
@@ -24,11 +25,11 @@ module Agents
24 25
       You can use an Event Formatting Agent's `instructions` setting to do this in the following way:
25 26
 
26 27
           "instructions": {
27
-            "message": "Today's conditions look like <$.conditions> with a high temperature of <$.high.celsius> degrees Celsius.",
28
-            "subject": "$.data"
28
+            "message": "Today's conditions look like {{conditions}} with a high temperature of {{high.celsius}} degrees Celsius.",
29
+            "subject": "{{data}}"
29 30
           }
30 31
 
31
-      JSONPaths must be between < and > . Make sure that you don't use these symbols anywhere else.
32
+      FIXME Provide a link to a explanation on how to use liquid templating
32 33
 
33 34
       Events generated by this possible Event Formatting Agent will look like:
34 35
 
@@ -60,18 +61,18 @@ module Agents
60 61
       So you can use it in `instructions` like this:
61 62
 
62 63
           "instructions": {
63
-            "message": "Today's conditions look like <$.conditions> with a high temperature of <$.high.celsius> degrees Celsius according to the forecast at <$.pretty_date.time>.",
64
-            "subject": "$.data"
64
+            "message": "Today's conditions look like <$.conditions> with a high temperature of {{high.celsius}} degrees Celsius according to the forecast at {{pretty_date.time}}.",
65
+            "subject": "{{data}}"
65 66
           }
66 67
 
67 68
       If you want to retain original contents of events and only add new keys, then set `mode` to `merge`, otherwise set it to `clean`.
68 69
 
69 70
       By default, the output event will have `agent` and `created_at` fields added as well, reflecting the original Agent type and Event creation time.  You can skip these outputs by setting `skip_agent` and `skip_created_at` to `true`.
70 71
 
71
-      To CGI escape output (for example when creating a link), prefix with `escape`, like so:
72
+      To CGI escape output (for example when creating a link), use the Liquid `uri_escape` filter, like so:
72 73
 
73 74
           {
74
-            "message": "A peak was on Twitter in <$.group_by>.  Search: https://twitter.com/search?q=<escape $.group_by>"
75
+            "message": "A peak was on Twitter in {{group_by}}.  Search: https://twitter.com/search?q={{group_by | uri_escape}}"
75 76
           }
76 77
     MD
77 78
 
@@ -88,8 +89,8 @@ module Agents
88 89
     def default_options
89 90
       {
90 91
         'instructions' => {
91
-          'message' =>  "You received a text <$.text> from <$.fields.from>",
92
-          'some_other_field' => "Looks like the weather is going to be <$.fields.weather>"
92
+          'message' =>  "You received a text {{text}} from {{fields.from}}",
93
+          'some_other_field' => "Looks like the weather is going to be {{fields.weather}}"
93 94
         },
94 95
         'matchers' => [],
95 96
         'mode' => "clean",
@@ -106,7 +107,7 @@ module Agents
106 107
       incoming_events.each do |event|
107 108
         formatted_event = options['mode'].to_s == "merge" ? event.payload.dup : {}
108 109
         payload = perform_matching(event.payload)
109
-        options['instructions'].each_pair {|key, value| formatted_event[key] = Utils.interpolate_jsonpaths(value, payload) }
110
+        formatted_event.merge! interpolate_options(options['instructions'], payload)
110 111
         formatted_event['agent'] = Agent.find(event.agent_id).type.slice!(8..-1) unless options['skip_agent'].to_s == "true"
111 112
         formatted_event['created_at'] = event.created_at unless options['skip_created_at'].to_s == "true"
112 113
         create_event :payload => formatted_event

+ 3 - 13
app/models/agents/hipchat_agent.rb

@@ -1,6 +1,6 @@
1 1
 module Agents
2 2
   class HipchatAgent < Agent
3
-    include JsonPathOptionsOverwritable
3
+    include LiquidInterpolatable
4 4
 
5 5
     cannot_be_scheduled!
6 6
     cannot_create_events!
@@ -18,22 +18,17 @@ module Agents
18 18
       If you want your message to notify the room members change `notify` to "true".
19 19
       Modify the background color of your message via the `color` attribute (one of "yellow", "red", "green", "purple", "gray", or "random")
20 20
 
21
-      If you want to specify either of those attributes per event, you can provide a [JSONPath](http://goessner.net/articles/JsonPath/) for each of them (except the `auth_token`).
21
+      TODO: add a link to the wiki explaining how to use the Liquid templating
22 22
     MD
23 23
 
24 24
     def default_options
25 25
       {
26 26
         'auth_token' => '',
27 27
         'room_name' => '',
28
-        'room_name_path' => '',
29 28
         'username' => "Huginn",
30
-        'username_path' => '',
31 29
         'message' => "Hello from Huginn!",
32
-        'message_path' => '',
33 30
         'notify' => false,
34
-        'notify_path' => '',
35 31
         'color' => 'yellow',
36
-        'color_path' => '',
37 32
       }
38 33
     end
39 34
 
@@ -49,14 +44,9 @@ module Agents
49 44
     def receive(incoming_events)
50 45
       client = HipChat::Client.new(options[:auth_token])
51 46
       incoming_events.each do |event|
52
-        mo = merge_json_path_options event
47
+        mo = interpolate_options options, event.payload
53 48
         client[mo[:room_name]].send(mo[:username], mo[:message], :notify => mo[:notify].to_s == 'true' ? 1 : 0, :color => mo[:color])
54 49
       end
55 50
     end
56
-
57
-    private
58
-    def options_with_path
59
-      [:room_name, :username, :message, :notify, :color]
60
-    end
61 51
   end
62 52
 end

+ 11 - 0
db/migrate/20140426202023_migrate_hipchat_and_ef_agent_to_liquid.rb

@@ -0,0 +1,11 @@
1
+class MigrateHipchatAndEfAgentToLiquid < ActiveRecord::Migration
2
+  def change
3
+    Agent.where(:type => 'Agents::HipchatAgent').each do |agent|
4
+      LiquidMigrator.convert_all_agent_options(agent)
5
+    end
6
+    Agent.where(:type => 'Agents::EventFormattingAgent').each do |agent|
7
+      agent.options['instructions'] = LiquidMigrator.convert_hash(agent.options['instructions'], {:merge_path_attributes => true, :leading_dollarsign_is_jsonpath => true})
8
+      agent.save
9
+    end
10
+  end
11
+end

+ 57 - 0
lib/liquid_migrator.rb

@@ -0,0 +1,57 @@
1
+module LiquidMigrator
2
+  def self.convert_all_agent_options(agent)
3
+    agent.options = self.convert_hash(agent.options, {:merge_path_attributes => true, :leading_dollarsign_is_jsonpath => true})
4
+    agent.save!
5
+  end
6
+
7
+  def self.convert_hash(hash, options={})
8
+    options = {:merge_path_attributes => false, :leading_dollarsign_is_jsonpath => false}.merge options
9
+    keys_to_remove = []
10
+    hash.tap do |hash|
11
+      hash.each_pair do |key, value|
12
+        case value.class.to_s
13
+        when 'String', 'FalseClass', 'TrueClass'
14
+          path_key = "#{key}_path"
15
+          if options[:merge_path_attributes] && !hash[path_key].nil?
16
+            # replace the value if the path is present
17
+            value = hash[path_key] if hash[path_key].present?
18
+            # in any case delete the path attibute
19
+            keys_to_remove << path_key
20
+          end
21
+          hash[key] = LiquidMigrator.convert_string value, options[:leading_dollarsign_is_jsonpath]
22
+        when 'Hash'
23
+          # might want to make it recursive?
24
+        when 'Array'
25
+          # do we need it?
26
+        end
27
+      end
28
+        # remove the unneeded *_path attributes
29
+    end.select { |k, v| !keys_to_remove.include? k }
30
+  end
31
+
32
+  def self.convert_string(string, leading_dollarsign_is_jsonpath=false)
33
+    if string == true || string == false
34
+      # there might be empty *_path attributes for boolean defaults
35
+      string
36
+    elsif string[0] == '$' && leading_dollarsign_is_jsonpath
37
+      # in most cases a *_path attribute
38
+      convert_json_path string
39
+    else
40
+      # migrate the old interpolation syntax to the new liquid based
41
+      string.gsub(/<([^>]+)>/).each do
42
+        match = $1
43
+        if match =~ /\Aescape /
44
+          # convert the old escape syntax to a liquid filter
45
+          self.convert_json_path(match.gsub(/\Aescape /, '').strip, ' | uri_escape')
46
+        else
47
+          self.convert_json_path(match.strip)
48
+        end
49
+      end
50
+    end
51
+  end
52
+
53
+  def self.convert_json_path(string, filter = "")
54
+    "{{#{string[2..-1].gsub(/\.\*\Z/, '')}#{filter}}}"
55
+  end
56
+end
57
+

+ 73 - 0
spec/lib/liquid_migrator_spec.rb

@@ -0,0 +1,73 @@
1
+require 'spec_helper'
2
+
3
+describe LiquidMigrator do
4
+  describe "converting JSONPath strings" do
5
+    it "should work" do
6
+      LiquidMigrator.convert_string("$.data", true).should == "{{data}}"
7
+      LiquidMigrator.convert_string("$.data.test", true).should == "{{data.test}}"
8
+      LiquidMigrator.convert_string("$.data.test.*", true).should == "{{data.test}}"
9
+    end
10
+
11
+    it "should ignore strings which just contain a JSONPath" do
12
+      LiquidMigrator.convert_string("$.data").should == "$.data"
13
+      LiquidMigrator.convert_string(" $.data", true).should == " $.data"
14
+      LiquidMigrator.convert_string("lorem $.data", true).should == "lorem $.data"
15
+    end
16
+  end
17
+
18
+  describe "converting escaped JSONPath strings" do
19
+    it "should work" do
20
+      LiquidMigrator.convert_string("Received <$.content.text.*> from <$.content.name> .").should ==
21
+                                    "Received {{content.text}} from {{content.name}} ."
22
+      LiquidMigrator.convert_string("Weather looks like <$.conditions> according to the forecast at <$.pretty_date.time>").should ==
23
+                                    "Weather looks like {{conditions}} according to the forecast at {{pretty_date.time}}"
24
+    end
25
+
26
+    it "should convert the 'escape' method correctly" do
27
+      LiquidMigrator.convert_string("Escaped: <escape $.content.name>\nNot escaped: <$.content.name>").should ==
28
+                                    "Escaped: {{content.name | uri_escape}}\nNot escaped: {{content.name}}"
29
+    end
30
+  end
31
+
32
+  describe "migrating a hash" do
33
+    it "should convert every attribute" do
34
+      LiquidMigrator.convert_hash({'a' => "$.data", 'b' => "This is a <$.test>"}).should ==
35
+                                  {'a' => "$.data", 'b' => "This is a {{test}}"}
36
+    end
37
+    it "should work with leading_dollarsign_is_jsonpath" do
38
+      LiquidMigrator.convert_hash({'a' => "$.data", 'b' => "This is a <$.test>"}, leading_dollarsign_is_jsonpath: true).should ==
39
+                                  {'a' => "{{data}}", 'b' => "This is a {{test}}"}
40
+    end
41
+    it "should use the corresponding *_path attributes when using merge_path_attributes"do
42
+      LiquidMigrator.convert_hash({'a' => "default", 'a_path' => "$.data"}, {leading_dollarsign_is_jsonpath: true, merge_path_attributes: true}).should ==
43
+                                  {'a' => "{{data}}"}
44
+    end
45
+  end
46
+
47
+  describe "migrating an actual agent" do
48
+    before do
49
+      valid_params = {
50
+                        'auth_token' => 'token',
51
+                        'room_name' => 'test',
52
+                        'room_name_path' => '',
53
+                        'username' => "Huginn",
54
+                        'username_path' => '$.username',
55
+                        'message' => "Hello from Huginn!",
56
+                        'message_path' => '$.message',
57
+                        'notify' => false,
58
+                        'notify_path' => '',
59
+                        'color' => 'yellow',
60
+                        'color_path' => '',
61
+                      }
62
+
63
+      @agent = Agents::HipchatAgent.new(:name => "somename", :options => valid_params)
64
+      @agent.user = users(:jane)
65
+      @agent.save!
66
+    end
67
+
68
+    it "should work" do
69
+      LiquidMigrator.convert_all_agent_options(@agent)
70
+      @agent.reload.options.should == {"auth_token" => 'token', 'color' => 'yellow', 'notify' => false, 'room_name' => 'test', 'username' => '{{username}}', 'message' => '{{message}}'}
71
+    end
72
+  end
73
+end

+ 3 - 3
spec/models/agents/event_formatting_agent_spec.rb

@@ -6,8 +6,8 @@ describe Agents::EventFormattingAgent do
6 6
         :name => "somename",
7 7
         :options => {
8 8
             :instructions => {
9
-                :message => "Received <$.content.text.*> from <$.content.name> .",
10
-                :subject => "Weather looks like <$.conditions> according to the forecast at <$.pretty_date.time>"
9
+                :message => "Received {{content.text}} from {{content.name}} .",
10
+                :subject => "Weather looks like {{conditions}} according to the forecast at {{pretty_date.time}}"
11 11
             },
12 12
             :mode => "clean",
13 13
             :matchers => [
@@ -82,7 +82,7 @@ describe Agents::EventFormattingAgent do
82 82
     it "should allow escaping" do
83 83
       @event.payload[:content][:name] = "escape this!?"
84 84
       @event.save!
85
-      @checker.options[:instructions][:message] = "Escaped: <escape $.content.name>\nNot escaped: <$.content.name>"
85
+      @checker.options[:instructions][:message] = "Escaped: {{content.name | uri_escape}}\nNot escaped: {{content.name}}"
86 86
       @checker.save!
87 87
       @checker.receive([@event])
88 88
       Event.last.payload[:message].should == "Escaped: escape+this%21%3F\nNot escaped: escape this!?"

+ 4 - 9
spec/models/agents/hipchat_agent_spec.rb

@@ -1,22 +1,17 @@
1 1
 require 'spec_helper'
2
-require 'models/concerns/json_path_options_overwritable'
2
+require 'models/concerns/liquid_interpolatable'
3 3
 
4 4
 describe Agents::HipchatAgent do
5
-  it_behaves_like JsonPathOptionsOverwritable
5
+  it_behaves_like LiquidInterpolatable
6 6
 
7 7
   before(:each) do
8 8
     @valid_params = {
9 9
                       'auth_token' => 'token',
10 10
                       'room_name' => 'test',
11
-                      'room_name_path' => '',
12
-                      'username' => "Huginn",
13
-                      'username_path' => '$.username',
14
-                      'message' => "Hello from Huginn!",
15
-                      'message_path' => '$.message',
11
+                      'username' => "{{username}}",
12
+                      'message' => "{{message}}",
16 13
                       'notify' => false,
17
-                      'notify_path' => '',
18 14
                       'color' => 'yellow',
19
-                      'color_path' => '',
20 15
                     }
21 16
 
22 17
     @checker = Agents::HipchatAgent.new(:name => "somename", :options => @valid_params)

+ 31 - 0
spec/models/concerns/liquid_interpolatable.rb

@@ -0,0 +1,31 @@
1
+require 'spec_helper'
2
+
3
+shared_examples_for LiquidInterpolatable do
4
+  before(:each) do
5
+    @valid_params = {
6
+      "normal" => "just some normal text",
7
+      "variable" => "{{variable}}",
8
+      "text" => "Some test with an embedded {{variable}}",
9
+      "escape" => "This should be {{hello_world | uri_escape}}"
10
+    }
11
+
12
+    @checker = described_class.new(:name => "somename", :options => @valid_params)
13
+    @checker.user = users(:jane)
14
+
15
+    @event = Event.new
16
+    @event.agent = agents(:bob_weather_agent)
17
+    @event.payload = { :variable => 'hello', :hello_world => "Hello world"}
18
+    @event.save!
19
+  end
20
+
21
+  describe "interpolating liquid templates" do
22
+    it "should work" do
23
+      @checker.send(:interpolate_options, @checker.options, @event.payload).should == {
24
+          "normal" => "just some normal text",
25
+          "variable" => "hello",
26
+          "text" => "Some test with an embedded hello",
27
+          "escape" => "This should be Hello+world"
28
+      }
29
+    end
30
+  end
31
+end